﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cloo;
using System.Threading;
using Processing.Exceptions;

namespace Processing
{
    /// <summary>
    /// Bildet das Grundgerüst einer Kernel.
    /// </summary>
    public abstract class AKernel : IKernel
    {
        #region Konstanten
        private const int THREAD_MEMORY = 256 * 1024 * 1024;
        #endregion Konstanten

        #region Attribute
        private Processor processor;
        private static Dictionary<KeyValuePair<Processor, System.Type>, ComputeProgram> compiledProgramms = new Dictionary<KeyValuePair<Processor, System.Type>, ComputeProgram>();
        private static object compiledProgrammsSync = new object();
        private ComputeProgram program;

        private KernelState state;
        private Thread thread;
        private object syncLock = new object();

        private ComputeKernel kernel;
        private Dictionary<int, IIBuffer> debugBuffers = new Dictionary<int, IIBuffer>();
        private object debugBuffersSync = new object();
        private Dictionary<int, IIBuffer> buffers = new Dictionary<int, IIBuffer>();
        private object buffersSync = new object();

        private static Dictionary<Processor, ComputeCommandQueue> processorCommandQueues = new Dictionary<Processor, ComputeCommandQueue>();
        private static object processorCommandQueuesSync = new object();
        private ComputeCommandQueue commandQueue;
        #endregion Attribute

        #region Eigenschaften
        /// <summary>
        /// Gibt den Prozessor der Kernel an.
        /// </summary>
        public Processor Processor { get { return processor; } }

        /// <summary>
        /// Gibt den Quellcode des Programms an.
        /// </summary>
        protected abstract string ProgramSource { get; }

        /// <summary>
        /// Gibt den Namen des Programms an.
        /// </summary>
        protected abstract string ProgramName { get; }

        /// <summary>
        /// Gibt die Dimension der Kernel an.
        /// </summary>
        protected abstract long[] Dimensions { get; }

        /// <summary>
        /// Gibt an ob die Kerneldaten initialisiert sind.
        /// </summary>
        protected abstract bool IsValidInitialized { get; }

        /// <summary>
        /// Gibt den Status der Kernel an.
        /// </summary>
        public KernelState State { get { return state; } }
        #endregion Eigenschaften

        #region Methoden
        /// <summary>
        /// Instanziiert und intialisiert die Kernel.
        /// </summary>
        /// <param name="processor">Der Prozessor der Kernel.</param>
        public AKernel(Processor processor)
        {
            this.processor = processor;

            // Compilerechenzeit sparen.
            lock (compiledProgrammsSync)
            {
                if (compiledProgramms.ContainsKey(new KeyValuePair<Processor, System.Type>(processor, this.GetType())))
                {
                    program = compiledProgramms[new KeyValuePair<Processor, System.Type>(processor, this.GetType())];
                }
                else
                {
                    try
                    {
                        program = new ComputeProgram(processor.Context, ProgramSource);
                        program.Build(null, null, null, IntPtr.Zero);
                    }
                    catch (Exception e) { throw new KernelException("Die Kompilierung der Kernel '" + ProgramName + "' ist fehlgeschlagen.", e); }
                    compiledProgramms.Add(new KeyValuePair<Processor, System.Type>(processor, this.GetType()), program);
                }
            }

            // Die CommandQueue des jeweiligen Prozessors heraussuchen.
            lock (processorCommandQueuesSync)
            {
                if (processorCommandQueues.ContainsKey(processor))
                {
                    commandQueue = processorCommandQueues[processor];
                }
                else
                {
                    commandQueue = new ComputeCommandQueue(processor.Context, processor.Context.Devices[0], ComputeCommandQueueFlags.None);
                    processorCommandQueues.Add(processor, commandQueue);
                }
            }
        }

        /// <summary>
        /// Führt die asynchron Kernel aus.
        /// </summary>
        public void RunAsync()
        {
            lock (syncLock)
            {
                if (!IsValidInitialized) throw new KernelException("Der Kernel '" + ProgramName + "' müssen zuerst Daten und Buffer zugewiesen werden!");
                state = KernelState.Running;

                PrepareKernelExecution();

                thread = new Thread(new ParameterizedThreadStart(this.run), THREAD_MEMORY);
                thread.Name = ProgramName;
                thread.Start(true);
            }
        }

        /// <summary>
        /// Führt die Kernel aus.
        /// </summary>
        public void Run()
        {
            lock (syncLock)
            {
                if (!IsValidInitialized) throw new KernelException("Der Kernel '" + ProgramName + "' müssen zuerst Daten und Buffer zugewiesen werden!");
                state = KernelState.Running;

                run((object)false);
            }
        }

        /// <summary>
        /// Führt die Kernel aus.
        /// </summary>
        /// <param name="async">Gibt an ob die Kernel asynchron ausgeführt wird.</param>
        private void run(object async)
        {
            try
            {
                if (!(bool)async) PrepareKernelExecution();

                // Handle für Kernel erstellen auf basis des Programms.
                kernel = program.CreateKernel(ProgramName);
                SetArguments();

                // Ausführungskontext finden.
                ComputeEventList eventList = new ComputeEventList();

                // Hier wird die Kernel selbst ausgeführt mit den angegebenen Dimensionen.
                // Um dabei Synchronität herzustellen wird in der Ereignisreihenfolge eine
                // Barriere und dann die Kernel eingefügt.
                lock (processorCommandQueuesSync)
                {
                    commandQueue.AddBarrier();
                    commandQueue.Execute(kernel, null, Dimensions, null, eventList);
                }

                // Warten bis Kernel fertig.
                eventList.Wait();            //  commandQueue.Finish();

                // Assignen der commandQueue damit Buffer gelesen werden können.
                foreach (KeyValuePair<int, IIBuffer> kvp in buffers) kvp.Value.RegisterQueue(commandQueue, eventList);
                kernel = null;

                if (processor.DebugMode)
                {
                    if (debugBuffers.Count > 0)
                    {
                        foreach (KeyValuePair<int, IIBuffer> kvp in debugBuffers)
                            try { General.ConsolePrinter.Write(kvp.Value.Data == null ? "!" : "."); }
                            catch { General.ConsolePrinter.Write("?"); }
                    }
                    else throw new KernelException("Die Kernel '" + ProgramName + "' unterstützt keinen Debugmodus.");
                }
                // Rücktransport der Ergebnisse.
                FinishKernelExecution();

                state = KernelState.Done;
            }
            catch (Exception e)
            {
                state = KernelState.Failed;
                throw new KernelException("Die Ausführung der Kernel '" + ProgramName + "' ist fehlgeschlagen.", e);
            }
        }

        #region SetArgument
        /// <summary>
        /// Setzt ein Argument basierend auf Hostdatenadressen.
        /// </summary>
        /// <param name="index">Der Index.</param>
        /// <param name="dataSize">Die Größe der Daten.</param>
        /// <param name="dataAddress">Die Adresse der Daten.</param>
        protected void SetArgument(int index, IntPtr dataSize, IntPtr dataAddress)
        {
            if (kernel == null) throw new KernelException("Dieser Ausruf kann nur von der Methode 'SetArguments' aus ausgeführt werden.");
            kernel.SetArgument(index, dataSize, dataAddress);
        }

        /// <summary>
        /// Setzt ein Argument basierend auf einer bestimmten Größe für den __local Speicher. (Achtung: Wird nicht von jeder GPU unterstützt.)
        /// </summary>
        /// <param name="index">Der Index.</param>
        /// <param name="dataSize">Die Länge der Daten.</param>
        protected void SetArgument(int index, long dataSize)
        {
            if (kernel == null) throw new KernelException("Dieser Ausruf kann nur von der Methode 'SetArguments' aus ausgeführt werden.");
            kernel.SetLocalArgument(index, dataSize);
        }

        /// <summary>
        /// Setzt ein Argument basierend auf den angegebenen Daten, die direkt übertragen werden.
        /// </summary>
        /// <typeparam name="T">Der Typ.</typeparam>
        /// <param name="index">Der Index.</param>
        /// <param name="data">Die Daten.</param>
        protected void SetArgument<T>(int index, T data) where T : struct
        {
            if (kernel == null) throw new KernelException("Dieser Ausruf kann nur von der Methode 'SetArguments' aus ausgeführt werden.");
            kernel.SetValueArgument(index, data);
        }

        /// <summary>
        /// Setzt ein Argument basierend auf dem angegebenen Sampler. (Achtung: Nur OpenCL 1.1)
        /// </summary>
        /// <param name="index">Der Index.</param>
        /// <param name="sampler">Der Sampler.</param>
        protected void SetArgument(int index, ComputeSampler sampler)
        {
            if (kernel == null) throw new KernelException("Dieser Ausruf kann nur von der Methode 'SetArguments' aus ausgeführt werden.");
            kernel.SetSamplerArgument(index, sampler);
        }

        /// <summary>
        /// Setzt ein Argument basierend auf dem angegebenen Buffer.
        /// </summary>
        /// <typeparam name="T">Der Typ.</typeparam>
        /// <param name="index">Der Index.</param>
        /// <param name="buffer">Der Buffer.</param>
        protected void SetArgument<T>(int index, Buffer<T> buffer) where T : struct
        {
            if (buffer == null) throw new ArgumentNullException("buffer");
            if (kernel == null) throw new KernelException("Dieser Ausruf kann nur von der Methode 'SetArguments' aus ausgeführt werden.");
            if (processor != buffer.Processor) throw new KernelException("Der angegebene Buffer gehört nicht zum Prozessor auf dem diese Kernel ausgeführt werden soll.");
            lock (buffersSync) { if (buffers.ContainsKey(index)) buffers[index] = buffer; else buffers.Add(index, buffer); }
            kernel.SetMemoryArgument(index, buffer.ComputeBuffer);
        }

        /// <summary>
        /// Setzt ein Argument basierend auf dem angegebenen Buffer.
        /// </summary>
        /// <typeparam name="T">Der Typ.</typeparam>
        /// <param name="index">Der Index.</param>
        /// <param name="buffer">Der Buffer.</param>
        /// <param name="debug">Der Indikator, ob die Variable zum Debugging verwendet werden soll.</param>
        protected void SetArgument<T>(int index, Buffer<T> buffer, bool debug) where T : struct
        {
            if (buffer == null) throw new ArgumentNullException("buffer");
            if (kernel == null) throw new KernelException("Dieser Ausruf kann nur von der Methode 'SetArguments' aus ausgeführt werden.");
            if (processor != buffer.Processor) throw new KernelException("Der angegebene Buffer gehört nicht zum Prozessor auf dem diese Kernel ausgeführt werden soll.");
            if (debug) lock (debugBuffersSync) { if (debugBuffers.ContainsKey(index)) debugBuffers[index] = buffer; else debugBuffers.Add(index, buffer); }
            lock (buffersSync) { if (buffers.ContainsKey(index)) buffers[index] = buffer; else buffers.Add(index, buffer); }
            kernel.SetMemoryArgument(index, buffer.ComputeBuffer);
        }
        #endregion SetArgument

        /// <summary>
        /// Führt Aktionen vor der Ausführung der Kernel - mit dem Run-Aufruf aus. (z. B. Datentransport von CPU zu GPU)
        /// </summary>
        protected virtual void PrepareKernelExecution() { }
        /// <summary>
        /// Setzt die Daten in Kernelargumente fest.
        /// </summary>
        protected abstract void SetArguments();

        /// <summary>
        /// Führt Aktionen nach der Ausführung der Kernel - mit dem Run-Aufruf aus. (z. B. Datentransport von GPU zu CPU)
        /// </summary>
        protected virtual void FinishKernelExecution() { }

        /// <summary>
        /// Gibt das Objekt für den GarbageCollector frei.
        /// </summary>
        public virtual void Dispose()
        {
            if (kernel != null) kernel.Dispose();
            kernel = null;
            processor = null;
            program = null;
            thread = null;
            syncLock = null;
            debugBuffers = null;
            debugBuffersSync = null;
            commandQueue = null;
        }

        /// <summary>
        /// Gibt die Zeichenkettenrepräsentation einer Kernel zurück.
        /// </summary>
        /// <returns>Die Zeichenkette.</returns>
        public override string ToString()
        {
            return ProgramName;
        }
        #endregion Methoden
    }
}
